package jmine.tec.utils.reflection.fast.asm;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Map;
import java.util.TreeMap;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

/**
 * Classe auxiliar para facilitar boxing / unboxing de primitivas
 * 
 * @author takeshi
 */
public final class BytecodeHelper implements Opcodes {

    /**
     * C'tor privado
     */
    private BytecodeHelper() {
    }

    private static final int MAX_INT_CONST = 5;

    private static final String JAVA_LANG_DOUBLE = "java/lang/Double";

    private static final String JAVA_LANG_FLOAT = "java/lang/Float";

    private static final String JAVA_LANG_LONG = "java/lang/Long";

    private static final String JAVA_LANG_BOOLEAN = "java/lang/Boolean";

    private static final String JAVA_LANG_INTEGER = "java/lang/Integer";

    private static final String JAVA_LANG_BYTE = "java/lang/Byte";

    private static final String JAVA_LANG_SHORT = "java/lang/Short";

    private static final String JAVA_LANG_CHAR = "java/lang/Character";

    private static final String JAVA_LANG_NUMBER = "java/lang/Number";

    /**
     * @param visitor {@link MethodVisitor} host
     * @param sourceType o tipo primitivo
     */
    public static void autoBox(MethodVisitor visitor, Class<?> sourceType) {
        if (sourceType == byte.class) {
            boxByte(visitor);
        } else if (sourceType == short.class) {
            boxShort(visitor);
        } else if (sourceType == char.class) {
            boxChar(visitor);
        } else if (sourceType == float.class) {
            boxFloat(visitor);
        } else if (sourceType == double.class) {
            boxDouble(visitor);
        } else if (sourceType == long.class) {
            boxLong(visitor);
        } else if (sourceType == int.class) {
            boxInt(visitor);
        } else if (sourceType == boolean.class) {
            boxBoolean(visitor);
        } else {
            throw new IllegalStateException("type not treated: " + sourceType);
        }
    }

    /**
     * @param visitor {@link MethodVisitor}
     */
    private static void boxLong(MethodVisitor visitor) {
        visitor.visitMethodInsn(INVOKESTATIC, JAVA_LANG_LONG, "valueOf", "(J)Ljava/lang/Long;");
    }

    /**
     * @param visitor {@link MethodVisitor}
     */
    private static void boxDouble(MethodVisitor visitor) {
        visitor.visitMethodInsn(INVOKESTATIC, JAVA_LANG_DOUBLE, "valueOf", "(D)Ljava/lang/Double;");
    }

    /**
     * @param visitor {@link MethodVisitor}
     */
    private static void boxFloat(MethodVisitor visitor) {
        visitor.visitMethodInsn(INVOKESTATIC, JAVA_LANG_FLOAT, "valueOf", "(F)Ljava/lang/Float;");
    }

    /**
     * @param visitor {@link MethodVisitor}
     */
    private static void boxChar(MethodVisitor visitor) {
        visitor.visitMethodInsn(INVOKESTATIC, JAVA_LANG_CHAR, "valueOf", "(C)Ljava/lang/Character;");
    }

    /**
     * @param visitor {@link MethodVisitor}
     */
    private static void boxShort(MethodVisitor visitor) {
        visitor.visitMethodInsn(INVOKESTATIC, JAVA_LANG_SHORT, "valueOf", "(S)Ljava/lang/Short;");
    }

    /**
     * @param visitor {@link MethodVisitor}
     */
    private static void boxByte(MethodVisitor visitor) {
        visitor.visitMethodInsn(INVOKESTATIC, JAVA_LANG_BYTE, "valueOf", "(B)Ljava/lang/Byte;");
    }

    /**
     * @param visitor {@link MethodVisitor}
     */
    private static void boxBoolean(MethodVisitor visitor) {
        visitor.visitMethodInsn(INVOKESTATIC, JAVA_LANG_BOOLEAN, "valueOf", "(Z)Ljava/lang/Boolean;");
    }

    /**
     * Gera o codigo de unboxing
     * 
     * @param visitor {@link MethodVisitor}
     * @param targetType o tipo primitivo
     */
    public static void autoUnbox(MethodVisitor visitor, Class<?> targetType) {
        if (targetType == byte.class) {
            unboxByte(visitor);
        } else if (targetType == short.class) {
            unboxShort(visitor);
        } else if (targetType == char.class) {
            unboxChar(visitor);
        } else if (targetType == float.class) {
            unboxFloat(visitor);
        } else if (targetType == double.class) {
            unboxDouble(visitor);
        } else if (targetType == int.class) {
            unboxInteger(visitor);
        } else if (targetType == long.class) {
            unboxLong(visitor);
        } else if (targetType == boolean.class) {
            unboxBoolean(visitor);
        } else {
            throw new IllegalStateException("type not treated: " + targetType);
        }
    }

    /**
     * @param visitor {@link MethodVisitor}
     */
    private static void unboxDouble(MethodVisitor visitor) {
        visitor.visitTypeInsn(CHECKCAST, JAVA_LANG_NUMBER);
        visitor.visitMethodInsn(INVOKEVIRTUAL, JAVA_LANG_NUMBER, "doubleValue", "()D");
    }

    /**
     * @param visitor {@link MethodVisitor}
     */
    private static void unboxFloat(MethodVisitor visitor) {
        visitor.visitTypeInsn(CHECKCAST, JAVA_LANG_NUMBER);
        visitor.visitMethodInsn(INVOKEVIRTUAL, JAVA_LANG_NUMBER, "floatValue", "()F");
    }

    /**
     * @param visitor {@link MethodVisitor}
     */
    private static void unboxChar(MethodVisitor visitor) {
        visitor.visitTypeInsn(CHECKCAST, JAVA_LANG_CHAR);
        visitor.visitMethodInsn(INVOKEVIRTUAL, JAVA_LANG_CHAR, "charValue", "()C");
    }

    /**
     * @param visitor {@link MethodVisitor}
     */
    private static void unboxShort(MethodVisitor visitor) {
        visitor.visitTypeInsn(CHECKCAST, JAVA_LANG_NUMBER);
        visitor.visitMethodInsn(INVOKEVIRTUAL, JAVA_LANG_NUMBER, "shortValue", "()S");
    }

    /**
     * @param visitor {@link MethodVisitor}
     */
    private static void unboxByte(MethodVisitor visitor) {
        visitor.visitTypeInsn(CHECKCAST, JAVA_LANG_NUMBER);
        visitor.visitMethodInsn(INVOKEVIRTUAL, JAVA_LANG_NUMBER, "byteValue", "()B");
    }

    /**
     * @param visitor {@link MethodVisitor}
     */
    private static void unboxBoolean(MethodVisitor visitor) {
        final String owner = JAVA_LANG_BOOLEAN;
        visitor.visitTypeInsn(CHECKCAST, owner);
        visitor.visitMethodInsn(INVOKEVIRTUAL, owner, "booleanValue", "()Z");
    }

    /**
     * @param visitor {@link MethodVisitor}
     */
    public static void boxInt(MethodVisitor visitor) {
        visitor.visitMethodInsn(INVOKESTATIC, JAVA_LANG_INTEGER, "valueOf", "(I)Ljava/lang/Integer;");
    }

    /**
     * @param visitor {@link MethodVisitor}
     */
    public static void unboxInteger(MethodVisitor visitor) {
        // first element on the operand stack must be an Integer
        final String owner = JAVA_LANG_NUMBER;

        visitor.visitTypeInsn(CHECKCAST, owner);
        visitor.visitMethodInsn(INVOKEVIRTUAL, owner, "intValue", "()I");
    }

    /**
     * @param visitor {@link MethodVisitor}
     */
    public static void unboxLong(MethodVisitor visitor) {
        final String internalName = JAVA_LANG_NUMBER;
        visitor.visitTypeInsn(CHECKCAST, internalName);
        visitor.visitMethodInsn(INVOKEVIRTUAL, internalName, "longValue", "()J");
    }

    /**
     * Cria um construtor vazio que invoca um construtor vazio da superclasse
     * 
     * @param cw {@link ClassWriter}
     * @param superType {@link Class}
     */
    public static void createEmptyConstructor(ClassWriter cw, Class<?> superType) {
        MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
        mv.visitCode();
        mv.visitVarInsn(ALOAD, 0);
        mv.visitMethodInsn(INVOKESPECIAL, Type.getInternalName(superType), "<init>", "()V");
        mv.visitInsn(RETURN);
        mv.visitMaxs(0, 0);
        mv.visitEnd();
    }

    /**
     * Sobrescreve o metodo passado
     * 
     * @param writer {@link ClassWriter}
     * @param targetMethod {@link Method}
     * @return {@link MethodVisitor}
     */
    public static MethodVisitor overwritePublicMethod(ClassWriter writer, Method targetMethod) {
        Class<?>[] types = targetMethod.getExceptionTypes();
        String[] exc = new String[types.length];
        for (int i = 0; i < types.length; i++) {
            Class<?> class1 = types[i];
            exc[i] = Type.getInternalName(class1);
        }
        return writer.visitMethod(ACC_PUBLIC, targetMethod.getName(), Type.getMethodDescriptor(targetMethod), null, exc);
    }

    /**
     * Gera a chamada ao metodo de 1 argumento
     * 
     * @param mv o {@link MethodVisitor} que contera o metodo sendo chamado
     * @param method o metodo sendo chamado
     * @param targetIndex o indice do call target. Soh vai ser usado se o metodo nao for estatico.
     * @param argumentIndex o indice do argumento unico para a chamada do metodo
     */
    public static void generateSingleArgumentMethodCallReturnResult(MethodVisitor mv, Method method, final int targetIndex,
            final int argumentIndex) {
        generateSingleMethodArgumentCall(mv, method, targetIndex, argumentIndex);
        // the method might return 'void'
        BytecodeHelper.fixReturnType(mv, method);
        mv.visitInsn(ARETURN);
    }

    /**
     * Gera a chamada a um metodo com 1 argumento. Na pilha, sobra o resultado do metodo, se este nao devolver 'void'
     * 
     * @param mv o {@link MethodVisitor}
     * @param method o {@link Method}
     * @param targetIndex o alvo da invocacao
     * @param argumentIndex o argumento
     */
    public static void generateSingleMethodArgumentCall(MethodVisitor mv, Method method, final int targetIndex, final int argumentIndex) {
        final boolean statMethod = BytecodeHelper.isStatic(method);
        final int op;
        if (!statMethod) {
            mv.visitVarInsn(ALOAD, targetIndex);
            // check-cast the target of the method
            mv.visitTypeInsn(CHECKCAST, Type.getInternalName(method.getDeclaringClass()));
            op = method.getDeclaringClass().isInterface() ? INVOKEINTERFACE : INVOKEVIRTUAL;
        } else {
            op = INVOKESTATIC;
        }
        // push the single argument to the stack
        mv.visitVarInsn(ALOAD, argumentIndex);
        final Class<?> paramType = method.getParameterTypes()[0];
        // either unbox the primitive wrapper
        if (paramType.isPrimitive()) {
            autoUnbox(mv, paramType);
        } else {
            // or check-cast to the method param type
            mv.visitTypeInsn(CHECKCAST, Type.getInternalName(paramType));
        }
        // invoke the method
        mv.visitMethodInsn(op, Type.getInternalName(method.getDeclaringClass()), method.getName(), Type.getMethodDescriptor(method));
    }

    /**
     * @param method the method
     * @return boolean
     */
    public static boolean isStatic(Method method) {
        return Modifier.isStatic(method.getModifiers());
    }

    /**
     * 'arruma' o topo da pilha. Empilha 'null' caso o metodo passado devolva void ou faz o boxing de primitivas
     * 
     * @param mv o {@link MethodVisitor}
     * @param method o {@link Method}
     */
    public static void fixReturnType(MethodVisitor mv, Method method) {
        if (method.getReturnType() == void.class || method.getReturnType() == Void.class) {
            // push null to fill the stack
            mv.visitInsn(ACONST_NULL);
            // or the method could return a primitive
        } else if (method.getReturnType().isPrimitive()) {
            // box the primitive
            autoBox(mv, method.getReturnType());
        }
    }

    /**
     * @param all all methods
     * @param arity the desired arity
     * @return {@link Map}
     */
    public static Map<Integer, Method> collectMethodsWithArity(Method[] all, int arity) {
        Map<Integer, Method> map = new TreeMap<Integer, Method>();
        for (int i = 0; i < all.length; i++) {
            Method method = all[i];
            if (method.getParameterTypes().length == arity) {
                map.put(i, method);
            }
        }
        return map;
    }

    /**
     * faz o 'push' de um inteiro.
     * 
     * @param constant o valor
     * @param mv o {@link MethodVisitor}
     */
    public static void pushIntConst(int constant, MethodVisitor mv) {
        if (constant >= -1 && constant <= MAX_INT_CONST) {
            mv.visitInsn(ICONST_M1 + constant + 1);
        } else if (constant >= Byte.MIN_VALUE && constant <= Byte.MAX_VALUE) {
            mv.visitIntInsn(BIPUSH, constant);
        } else if (constant >= Short.MIN_VALUE && constant <= Short.MAX_VALUE) {
            mv.visitIntInsn(SIPUSH, constant);
        } else {
            mv.visitLdcInsn(Integer.valueOf(constant));
        }
    }

    /**
     * Faz o 'push' de um valor booleano
     * 
     * @param value boolean
     * @param mv {@link MethodVisitor}
     */
    public static void pushBooleanValue(boolean value, MethodVisitor mv) {
        pushIntConst(value ? 1 : 0, mv);
    }

    /**
     * @param mv the {@link MethodVisitor}
     * @param localVariableIndex int
     * @param parameterTypes array de {@link Class}
     */
    public static void generateExpandArrayToTypes(MethodVisitor mv, int localVariableIndex, final Class<?>[] parameterTypes) {
        for (int i = 0; i < parameterTypes.length; i++) {
            // push the array into the stack
            mv.visitVarInsn(ALOAD, localVariableIndex);
            // push the array index into the stack
            pushIntConst(i, mv);
            // pop both and push the array value
            mv.visitInsn(AALOAD);
            Class<?> type = parameterTypes[i];
            if (type.isPrimitive()) {
                // target parameter is a primitive. Must unbox
                autoUnbox(mv, type);
            } else {
                mv.visitTypeInsn(CHECKCAST, Type.getInternalName(type));
            }
        }
    }

    /**
     * Expande o array situado no indice 'localVariableIndex' para a invocacao do metodo passado
     * 
     * @param mv {@link MethodVisitor} que contera o metodo
     * @param method {@link Method}
     * @param localVariableIndex indice
     */
    public static void generateExpandArrayToCallMethod(MethodVisitor mv, Method method, int localVariableIndex) {
        generateExpandArrayToTypes(mv, localVariableIndex, method.getParameterTypes());
    }

    /**
     * Gera a chamada ao metodo passado
     * 
     * @param mv {@link MethodVisitor} onde a chamada estara
     * @param method o {@link Method}
     * @param targetIndex o indice do alvo da invocação. Soh eh usado se o metodo nao for estatico.
     * @param argsIndex o indice do array que contem os argumentos do metodo
     */
    public static void generateMethodCalling(MethodVisitor mv, Method method, int targetIndex, int argsIndex) {
        final int op;
        if (!isStatic(method)) {
            // push target of calling method to the stack
            mv.visitVarInsn(ALOAD, targetIndex);
            mv.visitTypeInsn(CHECKCAST, Type.getInternalName(method.getDeclaringClass()));
            op = method.getDeclaringClass().isInterface() ? INVOKEINTERFACE : INVOKEVIRTUAL;
        } else {
            op = INVOKESTATIC;
        }
        if (method.getParameterTypes().length > 0) {
            generateExpandArrayToCallMethod(mv, method, argsIndex);
        }
        mv.visitMethodInsn(op, Type.getInternalName(method.getDeclaringClass()), method.getName(), Type.getMethodDescriptor(method));
        fixReturnType(mv, method);
        mv.visitInsn(ARETURN);
    }

    /**
     * Gera a chamada ao metodo sem argumentos passado.
     * 
     * @param mv o {@link MethodVisitor} que vai 'conter' o metodo
     * @param method o methodo a ser invocado
     * @param targetIndex o indice do alvo da invocacao. Soh eh usado se o metodo nao eh estatico
     */
    public static void generateNoArgMethodCallReturnResult(MethodVisitor mv, Method method, final int targetIndex) {
        generateNoArgMethodCall(mv, method, targetIndex);
        fixReturnType(mv, method);
        mv.visitInsn(ARETURN);
    }

    /**
     * Gera a invocacao a uma chamada a metodo. No topo da pilha, sobra o retorno da invocacao se este nao for 'void'
     * 
     * @param mv {@link MethodVisitor}
     * @param method {@link Method}
     * @param targetIndex int
     */
    public static void generateNoArgMethodCall(MethodVisitor mv, Method method, final int targetIndex) {
        final boolean sm = isStatic(method);
        final int op;
        if (!sm) {
            // push the call target
            mv.visitVarInsn(ALOAD, targetIndex);
            // check-cast the target of the method
            mv.visitTypeInsn(CHECKCAST, Type.getInternalName(method.getDeclaringClass()));
            op = method.getDeclaringClass().isInterface() ? INVOKEINTERFACE : INVOKEVIRTUAL;
        } else {
            op = INVOKESTATIC;
        }
        mv.visitMethodInsn(op, Type.getInternalName(method.getDeclaringClass()), method.getName(), Type.getMethodDescriptor(method));
    }

    /**
     * Transforma um array de classes em um array de {@link Type}
     * 
     * @param types array de {@link Class}
     * @return array de {@link Type}
     */
    public static Type[] toTypeArray(Class<?>[] types) {
        Type[] array = new Type[types.length];
        for (int i = 0; i < array.length; i++) {
            array[i] = Type.getType(types[i]);
        }
        return array;
    }

    /**
     * Gera uma classe com um construtor vazio
     * 
     * @param clName o nome da classe
     * @param superType {@link Class}
     * @return {@link ClassWriter}
     */
    public static ClassWriter createClassWithEmptyConstructor(String clName, final Class<CompiledConstructor> superType) {
        ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        writer.visit(V1_5, ACC_PUBLIC + ACC_SUPER, clName.replaceAll("[.]", "/"), null, Type.getInternalName(superType), new String[]{});
        createEmptyConstructor(writer, superType);
        return writer;
    }

    /**
     * Coloca no topo do stack o array dado. Cria instrucoes para criar o array e pegar os .class das classes correspondentes.
     * 
     * @param params array de class
     * @param mv {@link MethodVisitor}
     */
    public static void pushClassArray(Class<?>[] params, MethodVisitor mv) {
        // push array length in the stack
        pushIntConst(params.length, mv);
        // create array of string
        mv.visitTypeInsn(ANEWARRAY, Type.getInternalName(Class.class));
        // the empty array is on the stack
        for (int i = 0; i < params.length; i++) {
            mv.visitInsn(DUP);
            // 2 arrays on the stack
            pushIntConst(i, mv);
            // 2 arrays 1 int on the stack
            pushClass(params[i], mv);
            // 2 arrays, 1 int, 1 class in the stack
            mv.visitInsn(AASTORE);
            // poped 1 array, the index and the class
        }
        // only the array should be in the stack
    }

    /**
     * Coloca no topo do stack o Class representando o tipo passado. Se o tipo passado for primitivo, pega o campo estatico correspondente.
     * 
     * @param original {@link Class}
     * @param mv {@link MethodVisitor}
     */
    public static void pushClass(Class<?> original, MethodVisitor mv) {
        if (original == boolean.class) {
            mv.visitFieldInsn(GETSTATIC, JAVA_LANG_BOOLEAN, "TYPE", "Ljava/lang/Class;");
        } else if (original == byte.class) {
            mv.visitFieldInsn(GETSTATIC, JAVA_LANG_BYTE, "TYPE", "Ljava/lang/Class;");
        } else if (original == short.class) {
            mv.visitFieldInsn(GETSTATIC, JAVA_LANG_SHORT, "TYPE", "Ljava/lang/Class;");
        } else if (original == char.class) {
            mv.visitFieldInsn(GETSTATIC, JAVA_LANG_CHAR, "TYPE", "Ljava/lang/Class;");
        } else if (original == int.class) {
            mv.visitFieldInsn(GETSTATIC, JAVA_LANG_INTEGER, "TYPE", "Ljava/lang/Class;");
        } else if (original == float.class) {
            mv.visitFieldInsn(GETSTATIC, JAVA_LANG_FLOAT, "TYPE", "Ljava/lang/Class;");
        } else if (original == long.class) {
            mv.visitFieldInsn(GETSTATIC, JAVA_LANG_LONG, "TYPE", "Ljava/lang/Class;");
        } else if (original == double.class) {
            mv.visitFieldInsn(GETSTATIC, JAVA_LANG_DOUBLE, "TYPE", "Ljava/lang/Class;");
        } else {
            mv.visitLdcInsn(Type.getType(original));
        }
    }
}
